using System;
using System.Collections.Generic;
using System.Text;

using Toenda.Foundation.Utility;
using Toenda.Foundation.Globalization;

namespace Toenda.Foundation {
	/// <summary>
	/// Extends the DateTime class
	/// </summary>
	public static class DateTimeExtensions {
		/// <summary>
		/// Convert a datetime to a filename string
		/// </summary>
		/// <param name="instance">The datetime instance.</param>
		/// <returns></returns>
		public static string ToFileNameString(this DateTime instance) {
			StringBuilder str = new StringBuilder();

			str.Append(instance.Year.ToString());
			str.Append(instance.Month.ToString().PadLeft(2, '0'));
			str.Append(instance.Day.ToString().PadLeft(2, '0'));
			str.Append(instance.Hour.ToString().PadLeft(2, '0'));
			str.Append(instance.Minute.ToString().PadLeft(2, '0'));
			str.Append(instance.Second.ToString().PadLeft(2, '0'));

			return str.ToString();
		}

		/// <summary>
		/// Returns a value that indicates if a date is a real date and does not contain the default value
		/// </summary>
		/// <param name="instance"></param>
		/// <returns></returns>
		public static bool IsRealDate(this DateTime instance) {
			return instance != DateTimeHelper.Default;
		}

		/// <summary>
		/// Compares a date to this instance and returns a positive or negative value or a null...
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="compareTo"></param>
		/// <returns></returns>
		public static long FastCompareTo(this DateTime instance, DateTime compareTo) {
			return (instance.Ticks - compareTo.Ticks);
		}

		/// <summary>
		/// Convert a date to double (base 1.1.1900)
		/// </summary>
		/// <param name="instance"></param>
		/// <returns></returns>
		public static double ToDouble(this DateTime instance) {
			DateTime dt = new DateTime(1900, 1, 1);
			TimeSpan span = instance - dt;

			return span.TotalDays;
		}

		/// <summary>
		/// Convert a date to double (base 1.1.1900)
		/// </summary>
		/// <param name="instance"></param>
		/// <returns></returns>
		public static int ToIntFormat(this DateTime instance) {
			return (
				instance.Year.ToString()
				+ instance.Month.ToString().PadLeft(2, '0')
				+ instance.Day.ToString().PadLeft(2, '0')
			).ToInt32();
		}

		/// <summary>
		/// Create a new date from properties
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="properties"></param>
		/// <returns></returns>
		public static DateTime ParseFromProperties(this DateTime instance, DateTimeProperties properties) {
			return new DateTime(properties.Year, properties.Month, properties.Day);
		}

		/// <summary>
		/// Create a new date from an in in format (yyyyMMdd)
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="date"></param>
		/// <returns></returns>
		public static DateTime ParseFromInt(this DateTime instance, int date) {
			return ParseByFormat(instance, date.ToString(), "yyyyMMdd");
		}

		/// <summary>
		/// Parse a string by a format (yyyyMMdd)
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="date"></param>
		/// <returns></returns>
		public static DateTime ParseByFormat(this DateTime instance, string date) {
			return ParseByFormat(instance, date, "yyyyMMdd");
		}

		/// <summary>
		/// Parse a string by a format
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="date"></param>
		/// <param name="format"></param>
		/// <returns></returns>
		public static DateTime ParseByFormat(this DateTime instance, string date, string format) {
			string tmp = format.CleanFromCharacters("0123456789");
			string tmpDate = date.CleanFromCharacters(tmp);

			if(!tmpDate.IsNumeric()) {
				return default(DateTime);
			}
			else {
				DateTimeProperties dtp = GetProperties(instance, date, format);
				return Calendar.CorrectDate(dtp.Year, dtp.Month, dtp.Day);
			}
		}

		/// <summary>
		/// Get the properties of an date time object
		/// </summary>
		/// <param name="instance"></param>
		/// <returns></returns>
		public static DateTimeProperties GetProperties(this DateTime instance) {
			return new DateTimeProperties() {
				Year = instance.Year,
				Month = instance.Month,
				Day = instance.Day
			};
		}

		/// <summary>
		/// Get the propertie items of a string-formated datetime
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="date"></param>
		/// <returns></returns>
		public static DateTimeProperties GetProperties(this DateTime instance, string date) {
			return GetProperties(instance, date, "yyyyMMdd");
		}

		/// <summary>
		/// Get the propertie items of a string-formated datetime
		/// </summary>
		/// <param name="instance"></param>
		/// <param name="date"></param>
		/// <param name="format"></param>
		/// <returns></returns>
		public static DateTimeProperties GetProperties(this DateTime instance, string date, string format) {
			int DT_LENGTH = format.Length;

			if(date.Length < DT_LENGTH) {
				date = date.PadLeft(DT_LENGTH, '0');
			}

			if(date.Length > DT_LENGTH) {
				date = date.Substring(0, DT_LENGTH);
			}

			switch(format) {
				case "MM/dd/yyyy":
					return new DateTimeProperties() {
						Year = date.Substring(6, 4).ToInt32(),
						Month = date.Substring(0, 2).ToInt32(),
						Day = date.Substring(3, 2).ToInt32()
					};

				case "yyyyMMdd":
				default:
					return new DateTimeProperties() {
						Year = date.Substring(0, 4).ToInt32(),
						Month = date.Substring(4, 2).ToInt32(),
						Day = date.Substring(6, 2).ToInt32()
					};
			}
		}

		/// <summary>
		/// Returns the very end of the given day (the last millisecond of the last hour for the given <see cref="DateTime"/>).
		/// </summary>
		public static DateTime EndOfDay(this DateTime date) {
			return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
		}

		/// <summary>
		/// Returns the Start of the given day (the first millisecond of the given <see cref="DateTime"/>).
		/// </summary>
		public static DateTime BeginningOfDay(this DateTime date) {
			return new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0);
		}

		/// <summary>
		/// Returns the same date (same Day, Month, Hour, Minute, Second etc) in the next calendar year. 
		/// If that day does not exist in next year in same month, number of missing days is added to the last day in same month next year.
		/// </summary>
		public static DateTime NextYear(this DateTime start) {
			var nextYear = start.Year + 1;
			var numberOfDaysInSameMonthNextYear = DateTime.DaysInMonth(nextYear, start.Month);

			if(numberOfDaysInSameMonthNextYear < start.Day) {
				var differenceInDays = start.Day - numberOfDaysInSameMonthNextYear;
				var dateTime = new DateTime(nextYear, start.Month, numberOfDaysInSameMonthNextYear, start.Hour, start.Minute, start.Second, start.Millisecond);
				return dateTime + differenceInDays.Days();
			}

			return new DateTime(nextYear, start.Month, start.Day, start.Hour, start.Minute, start.Second, start.Millisecond);
		}

		/// <summary>
		/// Returns the same date (same Day, Month, Hour, Minute, Second etc) in the previous calendar year.
		/// If that day does not exist in previous year in same month, number of missing days is added to the last day in same month previous year.
		/// </summary>
		public static DateTime PreviousYear(this DateTime start) {
			var previousYear = start.Year - 1;
			var numberOfDaysInSameMonthPreviousYear = DateTime.DaysInMonth(previousYear, start.Month);

			if(numberOfDaysInSameMonthPreviousYear < start.Day) {
				var differenceInDays = start.Day - numberOfDaysInSameMonthPreviousYear;
				var dateTime = new DateTime(previousYear, start.Month, numberOfDaysInSameMonthPreviousYear, start.Hour, start.Minute, start.Second, start.Millisecond);
				return dateTime + differenceInDays.Days();
			}

			return new DateTime(previousYear, start.Month, start.Day, start.Hour, start.Minute, start.Second, start.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> increased by 24 hours ie Next Day.
		/// </summary>
		public static DateTime NextDay(this DateTime start) {
			return start + 1.Days();
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> decreased by 24h period ie Previous Day.
		/// </summary>
		public static DateTime PreviousDay(this DateTime start) {
			return start - 1.Days();
		}

		/// <summary>
		/// Returns first next occurrence of specified <see cref="DayOfWeek"/>.
		/// </summary>
		public static DateTime Next(this DateTime start, DayOfWeek day) {
			do {
				start = start.NextDay();
			}
			while(start.DayOfWeek != day);

			return start;
		}

		/// <summary>
		/// Returns first next occurrence of specified <see cref="DayOfWeek"/>.
		/// </summary>
		public static DateTime Previous(this DateTime start, DayOfWeek day) {
			do {
				start = start.PreviousDay();
			}
			while(start.DayOfWeek != day);

			return start;
		}

		/// <summary>
		/// Increases supplied <see cref="DateTime"/> for 7 days ie returns the Next Week.
		/// </summary>
		public static DateTime WeekAfter(this DateTime start) {
			return start + 1.Weeks();
		}

		/// <summary>
		/// Decreases supplied <see cref="DateTime"/> for 7 days ie returns the Previous Week.
		/// </summary>
		public static DateTime WeekEarlier(this DateTime start) {
			return start - 1.Weeks();
		}

		/// <summary>
		/// Increases the <see cref="DateTime"/> object with given <see cref="TimeSpan"/> value.
		/// </summary>
		public static DateTime IncreaseTime(this DateTime startDate, TimeSpan toAdd) {
			return startDate + toAdd;
		}

		/// <summary>
		/// Decreases the <see cref="DateTime"/> object with given <see cref="TimeSpan"/> value.
		/// </summary>
		public static DateTime DecreaseTime(this DateTime startDate, TimeSpan toSubtract) {
			return startDate - toSubtract;
		}

		/// <summary>
		/// Returns the original <see cref="DateTime"/> with Hour part changed to supplied hour parameter.
		/// </summary>
		public static DateTime SetTime(this DateTime originalDate, int hour) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, hour, originalDate.Minute, originalDate.Second, originalDate.Millisecond);
		}

		/// <summary>
		/// Returns the original <see cref="DateTime"/> with Hour and Minute parts changed to supplied hour and minute parameters.
		/// </summary>
		public static DateTime SetTime(this DateTime originalDate, int hour, int minute) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, hour, minute, originalDate.Second, originalDate.Millisecond);
		}

		/// <summary>
		/// Returns the original <see cref="DateTime"/> with Hour, Minute and Second parts changed to supplied hour, minute and second parameters.
		/// </summary>
		public static DateTime SetTime(this DateTime originalDate, int hour, int minute, int second) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, hour, minute, second, originalDate.Millisecond);
		}

		/// <summary>
		/// Returns the original <see cref="DateTime"/> with Hour, Minute, Second and Millisecond parts changed to supplied hour, minute, second and millisecond parameters.
		/// </summary>
		public static DateTime SetTime(this DateTime originalDate, int hour, int minute, int second, int millisecond) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, hour, minute, second, millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Hour part.
		/// </summary>
		public static DateTime SetHour(this DateTime originalDate, int hour) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, hour, originalDate.Minute, originalDate.Second, originalDate.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Minute part.
		/// </summary>
		public static DateTime SetMinute(this DateTime originalDate, int minute) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, originalDate.Hour, minute, originalDate.Second, originalDate.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Second part.
		/// </summary>
		public static DateTime SetSecond(this DateTime originalDate, int second) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, originalDate.Hour, originalDate.Minute, second, originalDate.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Millisecond part.
		/// </summary>
		public static DateTime SetMillisecond(this DateTime originalDate, int millisecond) {
			return new DateTime(originalDate.Year, originalDate.Month, originalDate.Day, originalDate.Hour, originalDate.Minute, originalDate.Second, millisecond);
		}

		/// <summary>
		/// Returns original <see cref="DateTime"/> value with time part set to midnight (alias for <see cref="BeginningOfDay"/> method).
		/// </summary>
		public static DateTime Midnight(this DateTime value) {
			return value.BeginningOfDay();
		}

		/// <summary>
		/// Returns original <see cref="DateTime"/> value with time part set to Noon (12:00:00h).
		/// </summary>
		/// <param name="value">The <see cref="DateTime"/> find Noon for.</param>
		/// <returns>A <see cref="DateTime"/> value with time part set to Noon (12:00:00h).</returns>
		public static DateTime Noon(this DateTime value) {
			return value.SetTime(12, 0, 0, 0);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Year part.
		/// </summary>
		public static DateTime SetDate(this DateTime value, int year) {
			return new DateTime(year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Year and Month part.
		/// </summary>
		public static DateTime SetDate(this DateTime value, int year, int month) {
			return new DateTime(year, month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Year, Month and Day part.
		/// </summary>
		public static DateTime SetDate(this DateTime value, int year, int month, int day) {
			return new DateTime(year, month, day, value.Hour, value.Minute, value.Second, value.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Year part.
		/// </summary>
		public static DateTime SetYear(this DateTime value, int year) {
			return new DateTime(year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Month part.
		/// </summary>
		public static DateTime SetMonth(this DateTime value, int month) {
			return new DateTime(value.Year, month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond);
		}

		/// <summary>
		/// Returns <see cref="DateTime"/> with changed Day part.
		/// </summary>
		public static DateTime SetDay(this DateTime value, int day) {
			return new DateTime(value.Year, value.Month, day, value.Hour, value.Minute, value.Second, value.Millisecond);
		}

		/// <summary>
		/// Determines whether the specified <see cref="DateTime"/> is before then current value.
		/// </summary>
		/// <param name="current">The current value.</param>
		/// <param name="toCompareWith">Value to compare with.</param>
		/// <returns>
		/// 	<c>true</c> if the specified current is before; otherwise, <c>false</c>.
		/// </returns>
		public static bool IsBefore(this DateTime current, DateTime toCompareWith) {
			return current < toCompareWith;
		}

		/// <summary>
		/// Determines whether the specified <see cref="DateTime"/> value is After then current value.
		/// </summary>
		/// <param name="current">The current value.</param>
		/// <param name="toCompareWith">Value to compare with.</param>
		/// <returns>
		/// 	<c>true</c> if the specified current is after; otherwise, <c>false</c>.
		/// </returns>
		public static bool IsAfter(this DateTime current, DateTime toCompareWith) {
			return current > toCompareWith;
		}

		/// <summary>
		/// Returns the given <see cref="DateTime"/> with hour and minutes set At given values.
		/// </summary>
		/// <param name="current">The current <see cref="DateTime"/> to be changed.</param>
		/// <param name="hour">The hour to set time to.</param>
		/// <param name="minute">The minute to set time to.</param>
		/// <returns><see cref="DateTime"/> with hour and minute set to given values.</returns>
		public static DateTime At(this DateTime current, int hour, int minute) {
			return current.SetTime(hour, minute);
		}

		/// <summary>
		/// Returns the given <see cref="DateTime"/> with hour and minutes and seconds set At given values.
		/// </summary>
		/// <param name="current">The current <see cref="DateTime"/> to be changed.</param>
		/// <param name="hour">The hour to set time to.</param>
		/// <param name="minute">The minute to set time to.</param>
		/// <param name="second">The second to set time to.</param>
		/// <returns><see cref="DateTime"/> with hour and minutes and seconds set to given values.</returns>
		public static DateTime At(this DateTime current, int hour, int minute, int second) {
			return current.SetTime(hour, minute, second);
		}

		/// <summary>
		/// Sets the day of the <see cref="DateTime"/> to the first day in that month.
		/// </summary>
		/// <param name="current">The current <see cref="DateTime"/> to be changed.</param>
		/// <returns>given <see cref="DateTime"/> with the day part set to the first day in that month.</returns>
		public static DateTime FirstDayOfMonth(this DateTime current) {
			return current.SetDay(1);
		}

		/// <summary>
		/// Sets the day of the <see cref="DateTime"/> to the last day in that month.
		/// </summary>
		/// <param name="current">The current DateTime to be changed.</param>
		/// <returns>given <see cref="DateTime"/> with the day part set to the last day in that month.</returns>
		public static DateTime LastDayOfMonth(this DateTime current) {
			return current.SetDay(DateTime.DaysInMonth(current.Year, current.Month));
		}

		/// <summary>
		/// Adds the given number of business days to the <see cref="DateTime"/>.
		/// </summary>
		/// <param name="current">The date to be changed.</param>
		/// <param name="days">Number of business days to be added.</param>
		/// <returns>A <see cref="DateTime"/> increased by a given number of business days.</returns>
		public static DateTime AddBusinessDays(this DateTime current, int days) {
			var sign = Math.Sign(days);
			var unsignedDays = Math.Abs(days);
			for(var i = 0; i < unsignedDays; i++) {
				do {
					current = current.AddDays(sign);
				}
				while(current.DayOfWeek == DayOfWeek.Saturday ||
					   current.DayOfWeek == DayOfWeek.Sunday);
			}
			return current;
		}

		/// <summary>
		/// Subtracts the given number of business days to the <see cref="DateTime"/>.
		/// </summary>
		/// <param name="current">The date to be changed.</param>
		/// <param name="days">Number of business days to be subtracted.</param>
		/// <returns>A <see cref="DateTime"/> increased by a given number of business days.</returns>
		public static DateTime SubtractBusinessDays(this DateTime current, int days) {
			return AddBusinessDays(current, -days);
		}

		/// <summary>
		/// Determine if a <see cref="DateTime"/> is in the future.
		/// </summary>
		/// <param name="dateTime">The date to be checked.</param>
		/// <returns><c>true</c> if <paramref name="dateTime"/> is in the future; otherwise <c>false</c>.</returns>
		public static bool IsInFuture(this DateTime dateTime) {
			return dateTime > DateTime.Now;
		}

		/// <summary>
		/// Determine if a <see cref="DateTime"/> is in the past.
		/// </summary>
		/// <param name="dateTime">The date to be checked.</param>
		/// <returns><c>true</c> if <paramref name="dateTime"/> is in the past; otherwise <c>false</c>.</returns>
		public static bool IsInPast(this DateTime dateTime) {
			return dateTime < DateTime.Now;
		}
	}
}
